package com.anb.net;

import com.anb.common.Log;

import java.io.IOException;
import java.net.SocketOption;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

import static java.net.SocketOption.*;

public class ConnectionBase extends NetObject {
    public ConnectionBase(IRunner runner, SocketChannel channel) {
        this(runner, channel, HEAD_LEN);
    }
    public ConnectionBase(IRunner runner, SocketChannel channel, int headsize) {
        super(getChannelRemoteName(channel));
        try {
            channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
        } catch (IOException e) {
            Log.Get().error(getAlias() + " set TCP_NODELAY failed", e);
        }
        this.runner = runner;
        this.channel = channel;
        head = ByteBuffer.allocate(headsize);
    }

    public static String getChannelRemoteName(SocketChannel channel) {
        String name;
        try {
            name = channel.getRemoteAddress().toString();
        } catch (IOException e) {
            Log.Get().error("get connection name failed", e);
            name = "?connection?";
        }
        return name;
    }

    public static Class<?>[] getArgTypes(Object[] connectionArgs) {
        int argsLen = connectionArgs.length;
        int len = argsLen + 2;
        Class<?>[] types = new Class<?>[len];
        types[0] = IRunner.class;
        types[1] = SocketChannel.class;
        for (int i = 0; i < argsLen; i++) {
            types[i + 2] = connectionArgs[i].getClass();
        }
        return types;
    }

    private IRunner runner;

    private SocketChannel channel;
    public SocketChannel getChannel() {
        return channel;
    }

    public void start() throws IOException {
        channel.configureBlocking(false);
        runner.register(this);
    }

    @Override
    public void stop() {
        super.stop();
        runner.unregister(this);
        try {
            channel.close();
        } catch (IOException e) {
            Log.Get().error(getAlias() + " stop error", e);
        }
    }

    public static final int HEAD_LEN = 4;
    private ByteBuffer head;
    private ByteBuffer rest;

    protected void read(SelectionKey key) {
        while (true) {
            if (rest == null) {
                if (!read(channel, head)) {
                    key.cancel();
                    stop();
                    return;
                }
                if (head.hasRemaining()) {
                    return;
                }
                head.flip();
                int len = head.getInt();
                if (len < HEAD_LEN) {
                    String error = String.format(getAlias() + " segment length should 0X%08X >= %d", len, HEAD_LEN);
                    Log.Get().error(error);
                    stop();
                    return;
                }
                int sizeLimit = handleHead(head);
                if (sizeLimit < 0) {
                    stop();
                    return;
                }
                if (len > sizeLimit) {
                    String error = String.format(getAlias() + " segment length should 0X%08X <= 0X%08X", len, sizeLimit);
                    Log.Get().error(error);
                    stop();
                    return;
                }
                head.clear();
                rest = ByteBuffer.allocate(len - head.limit());
            }

            if (!read(channel, rest)) {
                key.cancel();
                stop();
                return;
            }
            if (rest.hasRemaining()) {
                return;
            }
            ByteBuffer segment = rest;
            rest = null;
            segment.flip();
            Log.Get().trace(getAlias() + " received " + (head.limit() + segment.limit()) + " bytes");
            handleBody(segment);
        }
    }

    public boolean read(SocketChannel channel, ByteBuffer buffer) {
        int read;
        try {
            read = channel.read(buffer);
        } catch (IOException e) {
            Log.Get().error(getAlias() + " read error", e);
            return false;
        }

        if (read == -1) {
            return false;
        }
        return true;
    }

    protected int handleHead(ByteBuffer buffer) {
        return 0;
    }

    protected void handleBody(ByteBuffer buffer) {
    }

    protected void sendLengthy(ByteBuffer[] segments) throws IOException {
        int len0 = segments.length;
        ByteBuffer[] buffers = new ByteBuffer[1 + len0];
        int len = 0;
        for (int i = 0; i < len0; i++) {
            len += segments[i].remaining();
            buffers[i + 1] = segments[i];
        }
        len += HEAD_LEN;
        Log.Get().trace(getAlias() + " sent " + len + " bytes");
        ByteBuffer head = ByteBuffer.allocate(HEAD_LEN)
                .putInt(len);
        head.flip();
        buffers[0] = head;
        try {
            channel.write(buffers);
        } catch (IOException e) {
            Log.Get().error(getAlias() + " send error", e);
            stop();
            throw e;
        }
    }
}
